import time, json, hashlib, requests, asyncio
from loguru import logger

from core.command_manager import CommandRes
from modules.goofish_api import GoofishAPIClient, ResponseCode


class SearchManager:
    def __init__(self, state: 'StateManager', browser_manager=None):
        """
        state: 当前账号状态字典，包含 cookies / is_logged_in / need_relogin 等字段
        browser_manager: 浏览器控制器，用于重新登录（可选）
        """
        self.state = state
        self.browser_manager = browser_manager
        self.appKey = "34839810"
        self.session = requests.Session()
        self.headers = {
            'accept': 'application/json',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36'
        }
        
        # 使用新的API客户端（支持完整的风控、登录过期、token过期处理）
        self.api_client = GoofishAPIClient(state, browser_manager)
        
        # ================== 本地缓存与黑名单 ==================
        # 结果缓存：按关键词维度缓存已见商品ID，避免重复上报；限制缓存长度
        self._seen_item_ids_by_keyword: dict[str, list[str]] = {}
        self._max_seen_per_keyword: int = 300
        
        # ⚠️ 黑名单过滤已统一在控制端（后端）进行，Agent端不再维护黑名单
        # 保留这些变量是为了兼容性（如果未来需要恢复Agent端过滤）
        self._blacklist_seller: set[str] = set()
        self._blacklist_title_keywords: set[str] = set()
        
        # ✅ PC无法下单的商品ID列表（只能手机端下单的商品）
        # 当订单创建失败返回 FAIL_BIZ_PC_CAN_NOT_TRADE 时，将商品ID加入此列表
        # 搜索时会自动过滤掉这些商品
        self._mobile_only_item_ids: set[str] = set()

        # Cookie验证：每N次搜索验证一次cookie有效性（防止使用过期cookie返回缓存结果）
        self._search_count_since_cookie_check = 0
        self._cookie_check_interval = 10  # 每10次搜索验证一次cookie

    def md5(self, s: str):
        return hashlib.md5(s.encode()).hexdigest()

    def gen_params(self, token, post_param):
        t = str(int(time.time() * 1000))
        sign = self.md5(f"{token}&{t}&{self.appKey}&{post_param}")
        return {'jsv': "2.7.2", 'appKey': self.appKey, 't': t, 'sign': sign}

    def get_token(self):
        """从cookie中获取token"""
        cookies = self.state.get("cookie", {})
        if not cookies or '_m_h5_tk' not in cookies:
            return ""
        return cookies['_m_h5_tk'].split('_')[0]

    def search(self, keyword, page=1):
        """执行搜索请求（要求已登录并有有效cookie）"""
        token = self.get_token()
        if not token:
            return {"ok": False, "reason": "no_token"}

        post_data = json.dumps({
            "pageNumber": page,
            "keyword": keyword,
            "fromFilter": False,
            "rowsPerPage": 30
        }, ensure_ascii=False)

        params = self.gen_params(token, post_data)
        req_params = {
            'jsv': '2.7.2',
            'appKey': params['appKey'],
            't': params['t'],
            'sign': params['sign'],
            'v': '1.0',
            'type': 'originaljson',
            'dataType': 'json',
            'api': 'mtop.taobao.idlemtopsearch.pc.search',
        }

        resp = self.session.post(
            'https://h5api.m.goofish.com/h5/mtop.taobao.idlemtopsearch.pc.search/1.0/',
            headers=self.headers, params=req_params, data={'data': post_data}, timeout=10
        )

        try:
            j = resp.json()
            if j.get('ret') and j['ret'][0].startswith('SUCCESS'):
                return {"ok": True, "items": j.get('data', {}).get('resultList', [])}
            else:
                return {"ok": False, "reason": "api_error", "raw": j}
        except Exception as e:
            logger.exception(e)
            return {"ok": False, "reason": "exception", "error": str(e)}

    async def handle_search(self, msg):
        """处理来自TCP的search命令（使用新API客户端，支持完整的异常处理）"""
        keyword = msg.data.get("keyword", "") if msg and msg.data else ""
        page = msg.data.get("page", 1) if msg and msg.data else 1
        sort_type = msg.data.get("sortType", None) if msg and msg.data else None
        publish_day = msg.data.get("publishDay", None) if msg and msg.data else None
        min_price = msg.data.get("min_price", 0) if msg and msg.data else 0
        max_price = msg.data.get("max_price", 0) if msg and msg.data else 0

        if not keyword:
            return CommandRes.Search().Fail(msg="搜索关键词为空")

        # ✅ 注意：封禁检查已在 goofish_api.request 中统一处理，这里不再重复检查
        # ✅ 检查是否正在处理搜索（包括风控验证中）
        # 如果work_status已经是searching，说明上一个搜索请求还在处理中（可能正在风控验证），拒绝新请求
        current_work_status = self.state.get("work_status")
        if current_work_status == "searching":
            logger.warning(f"⚠️  上一个搜索请求仍在处理中（可能正在风控验证），拒绝新搜索请求: {keyword}")
            return CommandRes.Search().Fail(
                msg="上一个搜索请求仍在处理中（可能正在风控验证），请稍后再试",
                data={"keyword": keyword, "error_reason": "搜索请求处理中"}
            )

        # 检查是否可以搜索
        ready, reason = self.state.is_ready_for_search()
        if not ready:
            # ✅ 改进：确保keyword包含在错误响应中，便于前端日志显示
            return CommandRes.Search().Fail(
                msg=f"无法搜索: {reason}",
                data={"keyword": keyword, "error_reason": reason}
            )

        try:
            # 设置搜索状态
            self.state.start_searching()
            logger.info(f"开始搜索: {keyword}, 页码: {page}")
            
            # ✅ 搜索前：优先使用已有的cookie（state中的cookie），不强制从浏览器获取
            # ✅ 关键优化：API搜索的意义就是不需要浏览器，应该优先使用已有cookie
            # ✅ 只有在以下情况才从浏览器获取cookie：
            #    1. state中没有cookie（首次启动）
            #    2. 搜索失败且返回SESSION_EXPIRED时（这时才需要刷新浏览器）
            current_cookies = self.state.get("cookie", {})
            if not current_cookies:
                # state中没有cookie，尝试从浏览器获取（但不强制浏览器可用）
                if self.browser_manager:
                    try:
                        # ✅ 使用auto_restart=False，避免浏览器死掉时重启导致搜索变慢
                        cookies = await self.browser_manager.get_cookie_dict(auto_restart=False)
                        if cookies:
                            logger.debug("✅ 从浏览器获取到cookie（state中无cookie）")
                        else:
                            logger.debug("⚠️ state中无cookie且浏览器无法获取，将使用空cookie尝试搜索（服务器会返回错误码）")
                    except Exception as e:
                        logger.debug(f"从浏览器获取cookie失败: {e}，将使用空cookie尝试搜索")
            else:
                # state中有cookie，直接使用，不访问浏览器
                logger.debug(f"✅ 使用state中已有的cookie（{len(current_cookies)}个）")
            
            # 使用新API客户端搜索（自动处理token过期、登录过期、风控等）
            kwargs = {}
            if sort_type:
                kwargs["sortType"] = sort_type
            if publish_day:
                kwargs["publishDay"] = publish_day
            
            # 价格区间传入 API 客户端，内部会按规则构建 searchFilter
            kwargs["min_price"] = min_price
            kwargs["max_price"] = max_price
            result = await self.api_client.search(keyword, page, **kwargs)
            
            # ✅ 添加详细日志，便于诊断问题
            import json
            result_str = json.dumps(result, ensure_ascii=False)
            logger.debug(f"搜索API响应:\n {result_str[:150] if len(result_str) > 150 else result_str}")
            
            # ✅ 统一错误处理：goofish_api 已处理所有错误码（token过期、登录过期、风控等）
            # 这里只需判断是否成功，失败时直接返回错误信息
            
            ret_codes = result.get("ret", [])
            if not ret_codes:
                logger.error(f"搜索API响应格式错误: {result}")
                return CommandRes.Search().Fail(msg="API响应格式错误", data={"keyword": keyword, "raw_response": result})
            
            code = ret_codes[0]
            
            # 成功：返回SUCCESS或SUCCESS开头的码
            if code == ResponseCode.SUCCESS or (isinstance(code, str) and code.startswith("SUCCESS")):
                data = result.get("data", {})
                raw_items = data.get("resultList", [])
                
                # ✅ 移除定期Cookie验证：减少不必要的API调用，避免触发风控
                # 原因：
                # 1. 数据新鲜度检查已经能够检测到Session过期（通过商品发布时间判断）
                # 2. 额外的get_user_id API调用会增加API调用频率，可能触发风控
                # 3. 如果Session真的过期，搜索API会返回错误或旧数据，此时会触发恢复流程
                # logger.debug("✅ 跳过定期Cookie验证以减少API调用")
                
                # ✅ 映射原始API响应到扁平结构（参考C# SearchItemMapper）
                items = self._map_search_items(raw_items)
                
                # ✅ 不再在Agent端过滤黑名单，统一在控制端（后端）过滤
                # 一、对比缓存，标记新商品并更新缓存（使用原始数据，不过滤）
                keyword_for_cache = keyword.strip()
                compared_items, new_count = self._compare_and_cache(keyword_for_cache, items)
                
                # 二、自动下单（可选）
                auto_order_cfg = msg.data.get("auto_order") if msg and msg.data else None
                auto_order_results = []
                if auto_order_cfg and auto_order_cfg.get("enable"):
                    order_price_limit = float(auto_order_cfg.get("order_price") or 0)
                    auto_order_results = await self._auto_order_if_needed(compared_items, order_price_limit)
                
                logger.success(f"搜索成功，原始 {len(raw_items)} 条，映射后 {len(items)} 条，新商品 {new_count} 条，自动下单执行 {len(auto_order_results)} 条（黑名单过滤在控制端进行）")
                
                # ⚠️ 【暂时禁用】检测数据新鲜度（防止Session过期返回的旧缓存数据）
                # ✅ 用户反馈：新商品可能间隔较长，程序关闭几小时后重新运行仍能检测出过期，可能是误判
                # ✅ 暂时禁用此功能，避免误判导致不必要的Session恢复
                # session_expired_detected = False
                # if items and len(items) > 0:  # ✅ 使用过滤前的原始数据
                #     from datetime import datetime as dt
                #     current_time = int(time.time())
                #     
                #     # ✅ 检查前5条原始记录的发布时间（过滤前）
                #     recent_items = items[:5]
                #     old_items_count = 0
                #     max_publish_time = 0
                #     
                #     for item in recent_items:
                #         publish_time = item.get('publish_time') or item.get('publishTime') or 0
                #         if publish_time:
                #             max_publish_time = max(max_publish_time, publish_time)
                #             # 如果发布时间超过3小时（10800秒）认为是旧数据（提高阈值避免误判）
                #             if current_time - publish_time > 10800:
                #                 old_items_count += 1
                #     
                #     # ✅ 优化判断逻辑：
                #     # 1. 至少3条是旧数据（超过3小时），且原始数据至少有5条（避免过滤后数据不足导致的误判）
                #     # 2. 最新商品发布时间超过3小时（而不是2小时），避免误判
                #     # 3. 如果最新商品发布时间在2-3小时之间，可能是正常情况（商品确实比较旧），不触发恢复
                #     time_diff_hours = (current_time - max_publish_time) / 3600 if max_publish_time > 0 else 0
                #     should_trigger_refresh = (
                #         len(items) >= 5 and 
                #         old_items_count >= 3 and 
                #         time_diff_hours > 3.0  # 提高阈值到3小时，避免误判
                #     )
                #     
                #     if should_trigger_refresh:
                #         session_expired_detected = True
                #         logger.warning("=" * 70)
                #         logger.warning("⚠️ 【数据新鲜度警告】检测到搜索结果是过期的匿名缓存数据！")
                #         logger.warning(f"   - 前5条原始记录中有 {old_items_count} 条是3小时前的旧数据")
                #         logger.warning(f"   - 最新商品发布于 {time_diff_hours:.1f} 小时前")
                #         logger.warning(f"   - 原因：Session/Cookie已过期，闲鱼返回了匿名用户的旧数据")
                #         logger.warning("=" * 70)
                #         
                #         # ✅ 限制Session恢复频率：检查是否距离上次恢复时间过短
                #         # 注意：time模块已在文件顶部导入，不需要再次导入
                #         last_refresh_time = self.state.get("last_session_refresh_time", 0)
                #         current_time_for_refresh = time.time()
                #         time_since_last_refresh = current_time_for_refresh - last_refresh_time
                #         min_refresh_interval = 300  # 5分钟
                #         
                #         if time_since_last_refresh < min_refresh_interval:
                #             remaining_seconds = int(min_refresh_interval - time_since_last_refresh)
                #             logger.warning(f"⚠️ 距离上次Session恢复仅{int(time_since_last_refresh)}秒，跳过本次恢复（至少需间隔{min_refresh_interval}秒）")
                #             logger.warning("⚠️ 继续使用当前搜索结果（可能包含旧数据）")
                #             session_expired_detected = False  # 不触发恢复，继续使用当前结果
                #         else:
                #             # 尝试静默重新登录（参考C#实现）
                #             logger.info("🔄 调用浏览器管理器的快速登录恢复Session...")
                #             if self.browser_manager:
                #                 try:
                #                     # 调用浏览器管理器的快速登录恢复
                #                     refresh_success = await self.browser_manager.refresh_and_check_login()
                #                     if refresh_success:
                #                         logger.success("✅ Session恢复成功，重新执行搜索...")
                #                         # ✅ Session恢复成功后，重新执行搜索并返回结果
                #                         try:
                #                             # 重新搜索
                #                             retry_result = await self.api_client.search(
                #                             keyword=keyword,
                #                             page=1,
                #                             min_price=msg.data.get("min_price") if msg and msg.data else None,
                #                             max_price=msg.data.get("max_price") if msg and msg.data else None
                #                             )
                #                             
                #                             # ✅ 检查API响应格式（search方法返回的是原始API响应，包含ret、data、msg字段）
                #                             retry_ret_codes = retry_result.get("ret", [])
                #                             if retry_ret_codes:
                #                                 retry_code = retry_ret_codes[0]
                #                                 # 成功：返回SUCCESS或SUCCESS开头的码
                #                                 if retry_code == ResponseCode.SUCCESS or (isinstance(retry_code, str) and retry_code.startswith("SUCCESS")):
                #                                     retry_data = retry_result.get("data", {})
                #                                     retry_raw_items = retry_data.get("resultList", [])
                #                                     
                #                                     # 重新处理搜索结果（不再过滤黑名单，统一在控制端过滤）
                #                                     retry_items = self._map_search_items(retry_raw_items)
                #                                     retry_compared_items, retry_new_count = self._compare_and_cache(keyword_for_cache, retry_items)
                #                                     
                #                                     logger.success(f"重新搜索成功，原始 {len(retry_raw_items)} 条，映射后 {len(retry_items)} 条，新商品 {retry_new_count} 条（黑名单过滤在控制端进行）")
                #                                     
                #                                     # ✅ 返回重新搜索的结果
                #                                     return CommandRes.Search().Success(
                #                                         data={
                #                                             'keyword': keyword,
                #                                             'items': retry_compared_items,
                #                                             'new_items': [it for it in retry_compared_items if it.get('is_new')],
                #                                             'total': len(retry_raw_items),
                #                                             'new_count': retry_new_count,
                #                                             'session_recovered': True  # ✅ 标记Session已恢复
                #                                         }
                #                                     )
                #                                 else:
                #                                     # ✅ 记录详细的错误信息
                #                                     retry_msg = retry_result.get("msg", f"API返回错误码: {retry_code}")
                #                                     retry_error_type = retry_result.get("error_type", "api_error")
                #                                     logger.warning(f"重新搜索失败: {retry_msg} (code={retry_code}, error_type={retry_error_type})")
                #                             else:
                #                                 logger.warning(f"重新搜索失败: API响应格式错误 (ret字段缺失), 响应: {retry_result}")
                #                         except Exception as retry_error:
                #                             logger.error(f"重新搜索异常: {retry_error}")
                #                             # 即使重新搜索失败，也返回当前结果（因为Session已恢复）
                #                             logger.info("✅ Session已恢复，返回当前搜索结果（下次搜索将使用新Session）")
                #                     else:
                #                         logger.warning("❌ Session恢复失败，需要手动重新登录")
                #                         self.state.set_error(f"⚠️ 数据已过期({time_diff_hours:.1f}h)，Session恢复失败，需重新登录")
                #                 except Exception as refresh_error:
                #                     logger.error(f"Session恢复过程异常: {refresh_error}")
                #             else:
                #                 logger.warning("❌ 浏览器管理器不可用，无法恢复Session")
                #                 self.state.set_error(f"⚠️ 数据已过期({time_diff_hours:.1f}h)，浏览器不可用，需重新登录")
                
                # ✅ 打印前3条搜索结果的详细信息（用于调试对比）
                if compared_items and len(compared_items) > 0:
                    logger.info("=" * 60)
                    logger.info(f"【搜索结果前3条详细信息】关键词: {keyword}")
                    logger.info("=" * 60)
                    for i, item in enumerate(compared_items[:3], 1):
                        item_id = item.get('item_id') or item.get('itemId') or item.get('id') or 'N/A'
                        title = (item.get('title') or 'N/A')[:50]  # 标题截取前50个字符
                        publish_time = item.get('publish_time') or item.get('publishTime') or 0
                        price = item.get('price') or 0
                        seller_nick = item.get('seller_nick') or item.get('sellerNick') or 'N/A'
                        is_new = item.get('is_new', False)
                        
                        # 转换发布时间为可读格式
                        from datetime import datetime as dt
                        if publish_time and isinstance(publish_time, (int, float)):
                            publish_date = dt.fromtimestamp(publish_time).strftime('%Y-%m-%d %H:%M:%S')
                        else:
                            publish_date = 'N/A'
                        
                        logger.info(f"[{i}] 商品ID: {item_id}")
                        logger.info(f"    标题: {title}")
                        logger.info(f"    价格: {price}")
                        logger.info(f"    卖家: {seller_nick}")
                        logger.info(f"    发布时间: {publish_date} (时间戳: {publish_time})")
                        logger.info(f"    是否新商品: {'✅ 是' if is_new else '否'}")
                        logger.info("-" * 60)
                    logger.info("=" * 60)
                
                self.state.stop_searching()

                # 搜索完成后，发送状态更新（确保work_status被更新）
                if self.browser_manager and self.browser_manager.tcp_client:
                    asyncio.create_task(self.browser_manager.tcp_client.send_status())
                
                # ✅ 从搜索命令中获取user_id（如果提供了）
                user_id = None
                if msg and msg.data:
                    user_id = msg.data.get("user_id")
                
                return CommandRes.Search().Success(
                    msg="搜索成功",
                    data={
                        "keyword": keyword,
                        "page": page,
                        "items": compared_items,  # ✅ 发送原始数据，黑名单过滤在控制端进行
                        "count": len(compared_items),
                        "new_count": new_count,
                        "auto_orders": auto_order_results,
                        "has_next": data.get("resultInfo", {}).get("hasNextPage", False),
                        "user_id": user_id  # ✅ 返回user_id（用于服务器端WebSocket推送过滤）
                    }
                )
            
            error_msg = result.get("msg", f"搜索失败: {code}")
            error_type = result.get("error_type", "api_error")
            error_code = 1  # 默认错误码
            
            # 根据错误类型设置错误码（用于前端识别）
            if error_type == "session_expired":
                error_code = 2  # 登录过期
            elif error_type == "token_error":
                error_code = 3  # Token错误
            elif error_type == "risk_control":
                error_code = 4  # 风控验证
            elif error_type == "account_banned":
                error_code = 5  # 账号被封禁
            elif error_type == "task_stopped":
                error_code = 6  # 任务已停止
            
            logger.error(f"搜索失败: {error_msg} (type={error_type}, code={code})")
            
            # ✅ 如果是风控验证失败或重试请求失败，重置搜索状态（允许继续尝试下次搜索）
            if error_type == "risk_control":
                self.state.stop_searching()  # 重置 work_status 为 idle，允许继续接收下次搜索任务
                logger.warning("⚠️  风控验证失败，已重置搜索状态，等待下次搜索任务")
            elif error_type in ["retry_error", "retry_timeout"]:
                # ✅ 重试请求失败（异常或超时），也需要重置状态，允许下次搜索
                self.state.stop_searching()
                logger.warning(f"⚠️  重试请求失败（{error_type}），已重置搜索状态，等待下次搜索任务")
            
            # ✅ 如果是账号封禁，发送状态更新并记录详细信息
            if error_type == "account_banned":
                logger.error("=" * 70)
                logger.error("❌ 账号已被闲鱼封禁，无法继续搜索")
                logger.error(f"风控URL: {result.get('raw_risk_url', 'N/A')}")
                logger.error("=" * 70)
                
                # 发送状态更新到控制端
                if self.browser_manager and self.browser_manager.tcp_client:
                    try:
                        asyncio.create_task(self.browser_manager.tcp_client.send_status(include_cookie=False))
                        logger.info("✅ 已发送账号封禁状态更新到控制端")
                    except Exception as e:
                        logger.warning(f"发送封禁状态失败: {e}")
            
            # ✅ 改进：确保keyword包含在错误响应中，便于前端日志显示
            return CommandRes.Search().Fail(
                msg=error_msg,
                code=error_code,
                data={
                    "keyword": keyword,  # ✅ 确保keyword始终包含在错误响应中
                    "error_type": error_type,
                    "api_code": code,
                    "banned": result.get("banned", False),  # 是否被封禁
                    "raw_response": result.get("raw_risk_response"),  # 如果是风控错误，包含完整响应
                    "raw_risk_url": result.get("raw_risk_url")  # 封禁URL
                }
            )
                
        except Exception as e:
            logger.exception(f"搜索异常: {e}")
            self.state.set_error(str(e))
            # ✅ 改进：确保keyword包含在异常响应中
            return CommandRes.Search().Fail(
                msg=f"搜索异常: {e}",
                data={"keyword": keyword, "error": str(e)}
            )
        finally:
            # ✅ 确保恢复状态（无论成功还是失败都要重置）
            # 注意：只有当前任务的状态才重置，避免重置其他并发任务的状态
            current_status = self.state.get("work_status")
            if current_status == "searching":
                self.state.stop_searching()
                logger.debug(f"✅ 搜索任务完成，已重置work_status为idle")
            
            # 关闭API客户端session
            # await self.api_client.close()  # 不关闭，保持连接复用

    # ================== 数据映射（参考C# SearchItemMapper）==================
    def _map_search_items(self, raw_items: list[dict]) -> list[dict]:
        """
        映射原始API响应到扁平结构（参考C# SearchItemMapper.Map）
        
        C# 映射逻辑：
        - resultItem.Data.Item.Main.ClickParam.Args -> item_id, price, keyword, page, position, publishTime, seller_id
        - resultItem.Data.Item.Main.ExContent -> ItemId, Title, PicUrl, UserNickName, Area
        
        Returns:
            映射后的items列表，每个item包含扁平化的字段
        """
        if not raw_items:
            return []
        
        mapped_items = []
        for raw_item in raw_items:
            try:
                # 提取嵌套结构：data.item.main
                data_obj = raw_item.get("data", {})
                item_obj = data_obj.get("item", {})
                main = item_obj.get("main", {})
                
                if not main:
                    logger.warning(f"搜索结果项缺少main字段: {raw_item}")
                    continue
                
                # 提取 clickParam.args
                click_param = main.get("clickParam", {})
                args = click_param.get("args", {}) if click_param else {}
                
                # 提取 exContent
                ex_content = main.get("exContent", {})
                
                # 构建扁平化的item对象（参考C# SearchItem字段）
                mapped_item = {
                    # 商品ID（优先使用exContent.ItemId，否则使用args.item_id）
                    "itemId": ex_content.get("ItemId") or args.get("item_id") or args.get("itemId"),
                    "id": ex_content.get("ItemId") or args.get("item_id") or args.get("itemId"),
                    "item_id": ex_content.get("ItemId") or args.get("item_id") or args.get("itemId"),
                    
                    # 标题
                    "title": ex_content.get("Title") or ex_content.get("title"),
                    
                    # 价格（从args.price解析）
                    "price": self._parse_price_from_args(args.get("price", "")),
                    "priceDO": args.get("price", ""),  # 保留原始值
                    
                    # 图片
                    "picUrl": ex_content.get("PicUrl") or ex_content.get("picUrl"),
                    "image_url": ex_content.get("PicUrl") or ex_content.get("picUrl"),
                    "picWidth": ex_content.get("PicWidth") or ex_content.get("picWidth"),
                    "picHeight": ex_content.get("PicHeight") or ex_content.get("picHeight"),
                    
                    # 卖家信息
                    "sellerId": args.get("seller_id") or args.get("sellerId"),
                    "seller_id": args.get("seller_id") or args.get("sellerId"),
                    "sellerNick": ex_content.get("UserNickName") or ex_content.get("userNickName"),
                    "seller_nick": ex_content.get("UserNickName") or ex_content.get("userNickName"),
                    "userNickName": ex_content.get("UserNickName") or ex_content.get("userNickName"),
                    "userAvatarUrl": ex_content.get("UserAvatarUrl") or ex_content.get("userAvatarUrl"),
                    
                    # 地区
                    "area": ex_content.get("Area") or ex_content.get("area"),
                    "location": ex_content.get("Area") or ex_content.get("area"),
                    
                    # 搜索相关
                    "keyword": args.get("keyword", ""),
                    "page": self._parse_int(args.get("page", "1"), 1),
                    "position": self._parse_int(args.get("position", "0"), 0),
                    
                    # 发布时间（Unix时间戳毫秒，转换为秒）
                    "publishTime": self._parse_publish_time(args.get("publishTime", "")),
                    "publish_time": self._parse_publish_time(args.get("publishTime", "")),
                    
                    # 其他字段
                    "tagName": args.get("tagname") or args.get("tagName"),
                    "tagname": args.get("tagname") or args.get("tagName"),  # 统一使用小写tagname
                    "hideUserInfo": ex_content.get("HideUserInfo") or ex_content.get("hideUserInfo", False),
                    "isAliMaMaAD": ex_content.get("IsAliMaMaAD") or ex_content.get("isAliMaMaAD", False),
                    "isAuction": ex_content.get("IsAuction") or ex_content.get("isAuction", False),
                }
                
                # 过滤掉没有itemId的项（数据不完整）
                if not mapped_item.get("itemId") and not mapped_item.get("id") and not mapped_item.get("item_id"):
                    logger.warning(f"搜索结果项缺少商品ID，跳过: {mapped_item.get('title', '未知标题')}")
                    continue
                
                mapped_items.append(mapped_item)
            except Exception as e:
                logger.warning(f"映射搜索结果项失败: {e}, raw_item: {raw_item}")
                continue
        
        return mapped_items
    
    def _parse_price_from_args(self, price_str: str) -> float:
        """解析价格字符串"""
        if not price_str:
            return 0.0
        return self._normalize_price(price_str)
    
    def _parse_int(self, value: str, default: int = 0) -> int:
        """解析整数"""
        try:
            return int(value) if value else default
        except (ValueError, TypeError):
            return default
    
    def _parse_publish_time(self, timestamp_str: str) -> int:
        """
        解析发布时间（Unix时间戳毫秒 -> 秒）
        参考C#: DateTimeOffset.FromUnixTimeMilliseconds(timestampMs).LocalDateTime
        """
        if not timestamp_str:
            return 0
        try:
            # 如果是毫秒时间戳，转换为秒
            timestamp_ms = int(float(timestamp_str))
            timestamp_s = timestamp_ms // 1000
            return timestamp_s
        except (ValueError, TypeError):
            return 0

    # ================== 商品过滤逻辑 ==================
    def _filter_mobile_only_items(self, items: list[dict]) -> list[dict]:
        """
        ✅ 仅过滤PC无法下单的商品（只能手机端下单）
        ⚠️ 黑名单过滤已统一在控制端（后端）进行，Agent端不再处理
        """
        if not items:
            return []
        if not self._mobile_only_item_ids:
            return items

        filtered: list[dict] = []
        for it in items:
            # ✅ 过滤PC无法下单的商品（只能手机端下单）
            item_id = str(it.get("itemId") or it.get("id") or it.get("item_id") or "").strip()
            if item_id and item_id in self._mobile_only_item_ids:
                logger.debug(f"过滤PC无法下单的商品: {item_id} - {it.get('title', '未知标题')}")
                continue
            
            filtered.append(it)
        return filtered
    
    def add_mobile_only_item(self, item_id: str):
        """
        添加PC无法下单的商品ID（只能手机端下单）
        当订单创建失败返回 FAIL_BIZ_PC_CAN_NOT_TRADE 时调用
        """
        if item_id:
            self._mobile_only_item_ids.add(str(item_id).strip())
            logger.info(f"✅ 已将商品 {item_id} 加入PC无法下单过滤列表（只能手机端下单）")

    def _compare_and_cache(self, keyword: str, items: list[dict]) -> tuple[list[dict], int]:
        """对比历史缓存，标记新商品，并更新缓存列表。返回（标记后的items，新商品数量）"""
        seen = self._seen_item_ids_by_keyword.setdefault(keyword, [])
        new_count = 0
        output: list[dict] = []

        for it in items:
            # 兼容不同字段：itemId / id / item_id
            item_id = str(it.get("itemId") or it.get("id") or it.get("item_id") or "").strip()
            if not item_id:
                # 跳过无法识别ID的项
                continue

            is_new = item_id not in seen
            if is_new:
                new_count += 1
                seen.append(item_id)
                # 控制缓存长度，避免无限增长
                if len(seen) > self._max_seen_per_keyword:
                    del seen[: len(seen) - self._max_seen_per_keyword]

            # 增加通用字段便于前端使用
            normalized_price = self._normalize_price(it.get("price") or it.get("priceDO"))
            output.append({
                **it,
                "id": item_id,
                "is_new": is_new,
                "price": normalized_price,
                "seller_nick": it.get("sellerNick") or it.get("seller_nick"),
            })

        return output, new_count

    def _normalize_price(self, price) -> float:
        """将价格字段规范为 float。支持字符串/字典结构。"""
        try:
            if isinstance(price, (int, float)):
                return float(price)
            if isinstance(price, dict):
                # 兼容 {text: "¥123"} 或 {priceText: "123"}
                text = price.get("text") or price.get("priceText") or price.get("value")
                if text is None:
                    return 0.0
                return self._parse_price_text(str(text))
            if isinstance(price, str):
                return self._parse_price_text(price)
        except Exception:
            return 0.0
        return 0.0

    def _parse_price_text(self, text: str) -> float:
        s = text.replace("¥", "").replace(",", "").strip()
        try:
            return float(s)
        except Exception:
            return 0.0

    # ================== 黑名单更新入口（由服务端下发调用） ==================
    def update_seller_blacklist(self, sellers: list[str]):
        self._blacklist_seller = set([str(s).strip() for s in sellers if s])
        logger.info(f"已更新卖家黑名单：{len(self._blacklist_seller)} 条")

    def update_title_blacklist(self, keywords: list[str]):
        self._blacklist_title_keywords = set([str(s).strip() for s in keywords if s])
        logger.info(f"已更新标题关键字黑名单：{len(self._blacklist_title_keywords)} 条")
    
    # ⚠️ 以下方法已不再使用（黑名单过滤统一在控制端进行）
    # 保留这些方法是为了兼容性，如果后端仍发送黑名单更新，Agent端会忽略
    async def handle_update_seller_blacklist(self, msg):
        """⚠️ 已废弃：黑名单过滤已统一在控制端（后端）进行"""
        logger.debug("收到卖家黑名单更新，但Agent端不再处理（统一在控制端过滤）")
        return CommandRes.Status().Success(msg="已忽略（统一在控制端过滤）")
    
    async def handle_update_title_blacklist(self, msg):
        """⚠️ 已废弃：黑名单过滤已统一在控制端（后端）进行"""
        logger.debug("收到标题黑名单更新，但Agent端不再处理（统一在控制端过滤）")
        return CommandRes.Status().Success(msg="已忽略（统一在控制端过滤）")

    # ================== 自动下单（占位实现） ==================
    async def _auto_order_if_needed(self, items: list[dict], order_price_limit: float) -> list[dict]:
        """
        对标 C# 逻辑：
        - order_price_limit == 0: 发现新商品就下单
        - order_price_limit > 0: 商品价格 <= limit 才下单
        仅对 is_new 的商品尝试下单；这里实现为占位（仅日志与结果记录），真正下单请接入 api_client 的下单接口。
        返回：[ {id, title, price, ordered: bool, reason} ... ]
        """
        results: list[dict] = []
        for it in items:
            if not it.get("is_new"):
                continue
            pid = it.get("id")
            price = float(it.get("price") or 0)
            title = it.get("title")

            should = (order_price_limit == 0) or (price > 0 and price <= order_price_limit)
            if not should:
                results.append({"id": pid, "title": title, "price": price, "ordered": False, "reason": "price_too_high"})
                continue

            try:
                # TODO: 调用真正的下单接口（占位）
                logger.info(f"[AUTO-ORDER] 准备下单: {pid} - {title} - {price}")
                # success = await self.api_client.create_order(pid, price)  # 占位方法，待实现
                success = False  # 目前仅记录，不真正下单
                results.append({"id": pid, "title": title, "price": price, "ordered": bool(success)})
            except Exception as e:
                logger.exception(e)
                results.append({"id": pid, "title": title, "price": price, "ordered": False, "reason": str(e)})
    
    async def _verify_cookie_validity(self):
        """
        验证cookie有效性
        通过调用一个需要登录的API（如获取用户ID）来检查cookie是否过期
        如果过期，尝试刷新浏览器session
        """
        if not self.api_client:
            return
        
        try:
            logger.debug("🔍 验证cookie有效性...")
            # 使用获取用户IDAPI验证cookie（这是一个需要登录的API）
            result = await self.api_client.get_user_id()
            
            ret_codes = result.get("ret", [])
            if ret_codes:
                code = ret_codes[0]
                # 如果返回session过期，说明cookie已失效
                if code == ResponseCode.FAIL_SYS_SESSION_EXPIRED:
                    logger.warning("⚠️ Cookie验证失败：session已过期，尝试刷新浏览器session...")
                    if self.browser_manager:
                        try:
                            refresh_success = await self.browser_manager.refresh_and_check_login()
                            if refresh_success:
                                logger.success("✅ Cookie刷新成功，已更新")
                            else:
                                logger.warning("❌ Cookie刷新失败，需要重新登录")
                        except Exception as e:
                            logger.error(f"刷新cookie失败: {e}")
                elif code == ResponseCode.SUCCESS or (isinstance(code, str) and code.startswith("SUCCESS")):
                    logger.debug("✅ Cookie验证通过：session有效")
        except Exception as e:
            logger.warning(f"Cookie验证过程出错: {e}")
